a tool for shared writing and social publishing
1import { createIdentity } from "actions/createIdentity";
2import { subscribeToPublication } from "app/lish/subscribeToPublication";
3import { drizzle } from "drizzle-orm/node-postgres";
4import { cookies } from "next/headers";
5import { redirect } from "next/navigation";
6import { NextRequest, NextResponse } from "next/server";
7import { createOauthClient } from "src/atproto-oauth";
8import { setAuthToken } from "src/auth";
9
10import { supabaseServerClient } from "supabase/serverClient";
11import { URLSearchParams } from "url";
12import {
13 ActionAfterSignIn,
14 parseActionFromSearchParam,
15} from "./afterSignInActions";
16import { pool } from "supabase/pool";
17
18type OauthRequestClientState = {
19 redirect: string | null;
20 action: ActionAfterSignIn | null;
21};
22
23export async function GET(
24 req: NextRequest,
25 props: { params: Promise<{ route: string; handle?: string }> },
26) {
27 const params = await props.params;
28 let client = await createOauthClient();
29 switch (params.route) {
30 case "metadata":
31 return NextResponse.json(client.clientMetadata);
32 case "jwks":
33 return NextResponse.json(client.jwks);
34 case "login": {
35 const searchParams = req.nextUrl.searchParams;
36 const handle = searchParams.get("handle") as string;
37 // Put originating page here!
38 let redirect = searchParams.get("redirect_url");
39 if (redirect) redirect = decodeURIComponent(redirect);
40 let action = parseActionFromSearchParam(searchParams.get("action"));
41 let state: OauthRequestClientState = { redirect, action };
42
43 // Revoke any pending authentication requests if the connection is closed (optional)
44 const ac = new AbortController();
45
46 const url = await client.authorize(handle || "https://bsky.social", {
47 scope: "atproto transition:generic transition:email",
48 signal: ac.signal,
49 state: JSON.stringify(state),
50 });
51
52 return NextResponse.redirect(url);
53 }
54 case "callback": {
55 const params = new URLSearchParams(req.url.split("?")[1]);
56
57 let redirectPath = "/";
58 try {
59 const { session, state } = await client.callback(params);
60 let s: OauthRequestClientState = JSON.parse(state || "{}");
61 redirectPath = decodeURIComponent(s.redirect || "/");
62 let { data: identity } = await supabaseServerClient
63 .from("identities")
64 .select()
65 .eq("atp_did", session.did)
66 .single();
67 if (!identity) {
68 let existingIdentity = (await cookies()).get("auth_token");
69 if (existingIdentity) {
70 let data = await supabaseServerClient
71 .from("email_auth_tokens")
72 .select("*, identities(*)")
73 .eq("id", existingIdentity.value)
74 .single();
75 if (data.data?.identity && data.data.confirmed)
76 await supabaseServerClient
77 .from("identities")
78 .update({ atp_did: session.did })
79 .eq("id", data.data.identity);
80
81 return handleAction(s.action, redirectPath);
82 }
83 const client = await pool.connect();
84 const db = drizzle(client);
85 identity = await createIdentity(db, { atp_did: session.did });
86 client.release();
87 }
88 let { data: token } = await supabaseServerClient
89 .from("email_auth_tokens")
90 .insert({
91 identity: identity.id,
92 confirmed: true,
93 confirmation_code: "",
94 })
95 .select()
96 .single();
97
98 if (token) await setAuthToken(token.id);
99
100 // Process successful authentication here
101 console.log("authorize() was called with state:", state);
102
103 console.log("User authenticated as:", session.did);
104 return handleAction(s.action, redirectPath);
105 } catch (e) {
106 redirect(redirectPath);
107 }
108 }
109 default:
110 return NextResponse.json({ error: "Invalid route" }, { status: 404 });
111 }
112}
113
114const handleAction = async (
115 action: ActionAfterSignIn | null,
116 redirectPath: string,
117) => {
118 let parsePath = decodeURIComponent(redirectPath);
119 let url;
120 if (parsePath.includes("://")) url = new URL(parsePath);
121 else url = new URL(decodeURIComponent(redirectPath), "https://example.com");
122 if (action?.action === "subscribe") {
123 let result = await subscribeToPublication(action.publication);
124 if (result.hasFeed === false)
125 url.searchParams.set("showSubscribeSuccess", "true");
126 }
127
128 let path = url.pathname;
129 if (url.search) path += url.search;
130 if (url.hash) path += url.hash;
131 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path);
132};